package com.tianji.remark.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.api.dto.remark.LikedTimesDTO;
import com.tianji.common.autoconfigure.mq.RabbitMqHelper;
import com.tianji.common.constants.MqConstants;
import com.tianji.common.utils.*;
import com.tianji.remark.constants.RedisConstants;
import com.tianji.remark.domain.dto.LikeRecordFormDTO;
import com.tianji.remark.domain.po.LikedRecord;
import com.tianji.remark.mapper.LikedRecordMapper;
import com.tianji.remark.service.ILikedRecordService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.connection.StringRedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

@RequiredArgsConstructor
@Service
public class LikeRecordServiceRedisImpl extends ServiceImpl<LikedRecordMapper, LikedRecord> implements ILikedRecordService {
    private final RabbitMqHelper rabbitMqHelper;
    private final StringRedisTemplate redisTemplate;

    /**
     * 基于数据实现点赞或取消点赞
     * @param recordDTO
     */
    public void addLikeRecordUseDb(LikeRecordFormDTO recordDTO){
        //1、基于前端过来的数据判断是点赞还是取消点赞
        if(recordDTO.getLiked()){
            //是点赞业务
            //查询数据库，判断是否点过赞
            //点赞者和被点赞的业务id就可以唯一确定一个点赞记录数据
            LikedRecord record = this.lambdaQuery()
                    .eq(LikedRecord::getUserId, UserContext.getUser())
                    .eq(LikedRecord::getBizId, recordDTO.getBizId())
                    .one();
            if (record!=null){
                //已经点过赞了，直接返回就行
                return;
            }
            //没有点赞过，创建点赞记录对象
            LikedRecord likedRecord = BeanUtils.copyBean(recordDTO, LikedRecord.class);
            likedRecord.setUserId(UserContext.getUser());
            this.save(likedRecord);
        }else{
            //取消点赞业务
            boolean success = this.remove(new LambdaQueryWrapper<LikedRecord>()
                    .eq(LikedRecord::getBizId, recordDTO.getBizId())
                    .eq(LikedRecord::getUserId, UserContext.getUser()));
            if(!success){
                //删除失败，说明原来就没有这个数据记录，不用取消点赞，直接返回就是
                return;
            }
        }
        //2、统计更新后数据库中的点赞数量
        Integer count = this.lambdaQuery()
                .eq(LikedRecord::getBizId, recordDTO.getBizId())
                .count();
        //3、发送MQ通知，通知具体的业务去实现点赞数据的增加
        rabbitMqHelper.send(
                MqConstants.Exchange.LIKE_RECORD_EXCHANGE,
                StringUtils.format(MqConstants.Key.LIKED_TIMES_KEY_TEMPLATE, recordDTO.getBizType()),
                LikedTimesDTO.of(recordDTO.getBizId(), count));
    }

    /**
     * 基于redis对点赞业务进行优化
     * @param recordDTO
     */
    @Override
    public void addLikeRecord(LikeRecordFormDTO recordDTO) {
        //1、基于前端传过来的参数判断是点赞还是取消点赞
        //创建boolean记录业务是否完成
        boolean flag = false;
        if(recordDTO.getLiked() == true){
            //是点赞业务
            //将业务保存到redis里面
            //拼接redis操作的key，其中BizId表示业务id，可以是多个业务的，比如是问答，笔记等，是一个通用的业务id
            String key = RedisConstants.LIKE_BIZ_KEY_PREFIX + recordDTO.getBizId();
            //将数据保存到redis里面，redis的value是用户id，表示该业务被该用户点赞了
            //这里基于redis的set的数据结构来实现，set集合是一个key对应多个value，可以实现多人点赞
            //返回的结果表示的是成功添加到结合中的元素数量，如果是返回值是0，表示添加失败
            Long result = redisTemplate.opsForSet().add(key, UserContext.getUser().toString());
            flag = result!=null && result >0;
        }else{
            //表示是去取消点赞业务
            //实现从redis中移除数据的业务
            //拼接key
            String key = RedisConstants.LIKE_BIZ_KEY_PREFIX + recordDTO.getBizId();
            //基于redis的set数据结构移除数据，
            //因为set结构，一个key对应多个value，且这里是只想移除该用户的点赞，所以remove中还要加上value
            Long result = redisTemplate.opsForSet().remove(key, UserContext.getUser().toString());
            flag = result!=null && result >0;
        }
        //2、判断是否执行成功，如果执行失败则直接返回
        if(!flag){
            return;
        }
        //3、如果执行成功，要重新从redis中统计点赞总数
        //使用redis中set结构的size属性，size是key对应的value大小，表示有多少人点过赞
        Long num = redisTemplate.opsForSet()
                .size(RedisConstants.LIKE_BIZ_KEY_PREFIX + recordDTO.getBizId());
        if(num==null){
            return;
        }
        //4.缓存点赞总数到redis，使用ZSet数据结构
        //likes:times:type:QA  ZSet相较于Set多关联了一个score字段，用于排序
        redisTemplate.opsForZSet().add(
                RedisConstants.LIKES_TIMES_KEY_PREFIX+recordDTO.getBizType(),//Key
                recordDTO.getBizId().toString(),//Member
                num//Score(likedTimes)
        );
    }

    private boolean like(LikeRecordFormDTO recordDTO) {
        //1.获取用户id
        Long user = UserContext.getUser();
        //2.获取key
        String key = RedisConstants.LIKE_BIZ_KEY_PREFIX + recordDTO.getBizId();
        //3.执行SADD命令
        Long result = redisTemplate.opsForSet().add(key, user.toString());
        return result!=null && result>0;
    }

    private boolean unlike(LikeRecordFormDTO recordDTO) {
        //1.获取用户id
        Long user = UserContext.getUser();
        //2.获取key
        String key = RedisConstants.LIKE_BIZ_KEY_PREFIX + recordDTO.getBizId();
        //3.执行SREM命令
        Long result = redisTemplate.opsForSet().remove(key, user.toString());
        return result!=null && result>0;
    }
    /***
     * 根据传过来的业务id(bizIds)，查询该用户给哪些业务点过赞(基于redis去查)
     * @param bizIds
     * @return
     */

    @Override
    public Set<Long> isBizLiked(List<Long> bizIds) {
        //1、获取当前用户id
        Long userId = UserContext.getUser();
        //2、查询点赞状态，使用pipline改造(使用背景:短时间执行大量redis命令)
        //使用StringRedisTemplate来执行一个管道（pipeline）操作
        //管道作为一种批量操作可以减少网络往返次数，提高redis的效率
        //executePipelined()：这是 StringRedisTemplate 提供的一个方法，
        // 它接受一个 RedisCallback 接口的实现作为参数，用于执行管道操作。
        List<Object> objects = redisTemplate.executePipelined((RedisCallback<Object>) connection -> {//lambda 表达式，内容是redis的操作
            //将 connection 转换为 StringRedisConnection 类型，以便使用字符串操作的方法。
            StringRedisConnection src = (StringRedisConnection) connection;
            for (Long bizId : bizIds) {
                //拼接key
                String key = RedisConstants.LIKE_BIZ_KEY_PREFIX + bizId;
                //检查 userId 是否是集合 key 的成员。sIsMember 方法用于判断给定的 userId 是否存在于指定的集合 key 中。
                // 这个方法的返回值是一个布尔值，表示 userId 是否点赞了对应的业务 ID
                src.sIsMember(key, userId.toString());
            }
            return null;
        });
        // 3.返回结果 使用stream流
        return IntStream.range(0, objects.size()) // 创建从0到集合size的流
                .filter(i -> (boolean) objects.get(i)) // 遍历每个元素，保留结果为true的角标i
                .mapToObj(bizIds::get)// 用角标i取bizIds中的对应数据，就是点赞过的id
                .collect(Collectors.toSet());// 收集
    }

    @Override
    public void readLikedTimesAndSendMessage(String bizType, int maxBizSize) {
        //1.拼接key likes:times:type:QA  likes:time:type:NOTE
        String bizTypeTotalLikeKey = RedisConstants.LIKES_TIMES_KEY_PREFIX + bizType;
        //2.从redis的zset结构中按分数排序取maxBizSize的业务点赞信息 使用popmin方法
        Set<ZSetOperations.TypedTuple<String>> typedTuples = redisTemplate.opsForZSet().popMin(bizTypeTotalLikeKey, maxBizSize);
        List<LikedTimesDTO> list = new ArrayList<>();
        for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
            String bizId = typedTuple.getValue();//业务id
            Double likedTimes = typedTuple.getScore();
            if(StringUtils.isBlank(bizId) || likedTimes==null){
                continue;//数据有问题，直接跳过
            }
            //3.封装likedTimeDTO 消息数据
            LikedTimesDTO msg = LikedTimesDTO.of(Long.valueOf(bizId), likedTimes.intValue());
            list.add(msg);
        }
        //4.发送消息到mq
        if(CollUtils.isNotEmpty(list)){
            rabbitMqHelper.send(
                    MqConstants.Exchange.LIKE_RECORD_EXCHANGE,//交换机，选择与点赞相关的交换机
                    StringUtils.format(MqConstants.Key.LIKED_TIMES_KEY_TEMPLATE,bizType),//routingKey,用类型拼接出来
                    list//发送list
            );
        }
    }
}
